package world;

import java.util.HashMap;
import java.util.HashSet;

import datatypes.XZasDouble;

import packets.SPacket;
import packets.s2cpackets.S0x32;
import packets.s2cpackets.S0x33;
import entities.Player;
import exceptions.MCConnectionException;
import tools.ArrayMerge;
import worldmap.MapColumn;
import worldmap.MapColumnChunk;
import worldmap.WorldMap;


/**
 * The mapmanager will keep track of the state of the map.
 * 
 * It monitors players and send players new map-parts if they need them.
 * It send map updates to players when they need them.
 * It will request clients to unload map-parts if they should do that.
 * 
 */
public final class MapManager extends WorldStateMonitor {

	private WorldMap worldmap = new WorldMap();
	private HashMap<Player, HashSet<Long>> playerLoadedColumns = new HashMap<Player, HashSet<Long>>();
	
	
	public MapManager(WorldState state) {
		super(state);
	}
	
	
	

	
	
	private void setPlayerHasLoadedColumn(Player player, int x, int z) {
		long coordinate = XZasDouble.toLong(x, z);
		
		if(!playerLoadedColumns.containsKey(player)) {
			playerLoadedColumns.put(player, new HashSet<Long>());
		}
		playerLoadedColumns.get(player).add(coordinate);
	}
	
	private void unsetPlayerHasLoadedColumn(Player player, int x, int z) {
		long coordinate = XZasDouble.toLong(x, z);
		
		try {
			playerLoadedColumns.get(player).remove(coordinate);
		} catch(Exception e) { } //nothing to do on catch
	}
	
	private boolean playerHasLoadedColumn(Player player, int x, int z) {
		long coordinate = XZasDouble.toLong(x, z);
		
		try {
			return 	playerLoadedColumns.get(player).contains(coordinate);
		} catch(Exception e) { } //nothing to do on catch
		return false ;
	}
	
	
	
	
	
	/**
	 * Sends the initial area of 7x7 columns to the player.
	 * This is always send before spawing or re-spawning.
	 * 
	 * @param player
	 * @throws MCConnectionException
	 */
	public void sendInitialMap(Player player) throws MCConnectionException {
		//first send needed init-chunks
		// in order to calculate the correct column, divide the location by 16
		int px = (int) player.getLocation().x / 16;
		int pz = (int) player.getLocation().z / 16;

		
		for(int x = px-3; x <= px+3; x++) {
			for(int z = pz-3; z <= pz+3; z++) {
				sendInitColumn(x, z, player);
			}
		}
		
		for(int x = px-3; x <= px+3; x++) {
			for(int z = pz-3; z <= pz+3; z++) {
				sendColumn(x, z, player);
			}
		}
		System.out.println("ALL DONE");
		//start a thread which will start sending new mapchunks when needed.
		//new Thread(new MapsendThread(player)).start();
	}

	
	/**
	 * Requests the client to initialize a new column. Note that this should
	 * sent before the column data itself.
	 * 
	 * @param x
	 * @param z
	 * @param player
	 * @throws MCConnectionException
	 */
	private void sendInitColumn(int x, int z, Player player) throws MCConnectionException {
		SPacket p = new S0x32(x, z, true);
		state.connectionManager().sendPacketTo(player, p);
		setPlayerHasLoadedColumn(player, x, z);
	}
	
	
	

	
	
	
	/**
	 * Send all nececairy data to the so it can build up the column for the given
	 * coordinates.
	 * 
	 * @param x
	 * @param z
	 * @param player
	 * @throws MCConnectionException
	 */
	private void sendColumn(int x, int z, Player player) throws MCConnectionException {
		MapColumn column = worldmap.getColumn(x, z);
		
		short maskerbit = 1;
		
		//sending 16 chunks from bottom up to top
		short bitmask = 0;
		short bitmaskaddarray = 0;
		
		byte[] block_ids= null;
		byte[] block_metadata = null;
		byte[] block_light = null;
		byte[] block_skylight= null;

		//TODO
		byte[] biome_addarray	= new byte[16*16];
		for(int i = 0; i<biome_addarray.length; i++) {
			biome_addarray[i] = 0x09;
		}
		
		
		//TODO?
		byte[] block_addarray	= new byte[0];
		
		
		for(byte y = 0; y < 16; y++) {
			if(!column.isAir(y)) {
				bitmask |= maskerbit;
				MapColumnChunk chunk = column.getChunk(y);
				if(block_ids == null) {
					block_ids = chunk.getBlocksIds();
					block_metadata = chunk.getBlocksMetadata();
					block_light = chunk.getBlocksLight();
					block_skylight = chunk.getBlocksSkylight();
				} else {
					block_ids = ArrayMerge.mergeBytes(block_ids, chunk.getBlocksIds());
					block_metadata = ArrayMerge.mergeBytes(block_metadata, chunk.getBlocksMetadata());
					block_light = ArrayMerge.mergeBytes(block_light, chunk.getBlocksLight());
					block_skylight = ArrayMerge.mergeBytes(block_skylight, chunk.getBlocksSkylight());
				}
				
			}
			maskerbit <<= maskerbit;
		}
		
		byte[] r = ArrayMerge.mergeBytes(block_ids, block_metadata);
		r = ArrayMerge.mergeBytes(r, block_light);
		r = ArrayMerge.mergeBytes(r, block_skylight);
		r = ArrayMerge.mergeBytes(r, block_addarray);
		r = ArrayMerge.mergeBytes(r, biome_addarray);		
		
	
		SPacket p = new S0x33(x, z, true, bitmask, bitmaskaddarray, r);
		System.out.println("L: "+r.length);
		state.connectionManager().sendPacketTo(player, p);
		
	}
	
	
	class AllSentException extends Exception {
		private static final long serialVersionUID = 1085415294421705987L;
	}
	
	
	/**
	 * Thread wich will ping a client for a undefined amount of time.
	 * Thread stops automagically and silently when a connection is
	 * closed or an error occurs.
	 */
	class MapsendThread implements Runnable {

		private Player player;
		
		public MapsendThread(Player player) {
			this.player = player;
		}
		
		private void sendColumn(int x, int z) {
			try {
				MapManager.this.sendInitColumn(x, z, player);
				MapManager.this.sendColumn(x, z, player);
			} catch (MCConnectionException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return;
			}
		}
		
		@Override
		public void run() {
			System.out.println("Starting the mapsendthread.");
			while(true) {
				try {
					sendNextColumn();
				} catch (AllSentException e) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
				}
			}
		}
		
		
		/**
		 * Decide which column has to be sent to the client.
		 * Then send it.
		 * @throws AllSentException 
		 */
		private void sendNextColumn() throws AllSentException {
			int cx = (int) (player.getLocation().x / 16);
			int cz = (int) (player.getLocation().z / 16);
			
			//fill a radius_max grid around the player with columns
			int radius_max = 15;
			for(int radius = 0; radius < radius_max; radius++) {
				int z_upper = cz + radius;
				int z_lower = cz - radius;
				
				for(int x = cx - radius; x < cx + radius; x++) {
					if(!playerHasLoadedColumn(player, x, z_upper)) {
						sendColumn(x, z_upper);
						return;
					}
					
					if(!playerHasLoadedColumn(player, x, z_lower)) {
						sendColumn(x, z_lower);		
						return;
					}
				}
				
				int x_upper = cx + radius;
				int x_lower = cx - radius;
				for(int z = cz - radius; z < cz + radius; z++) {
					if(!playerHasLoadedColumn(player, x_lower, z)) {
						sendColumn(x_lower, z);
						return;
					}
					
					if(!playerHasLoadedColumn(player, x_upper, z)) {
						sendColumn(x_upper, z);		
						return;
					}
					
				}
			}
			
			throw new AllSentException();
			
		}
		
	}

}
